終於進入第四章了!
第四章的主題是 "Designs and Declarations,是關於 設計與宣告好的C++介面的一些重點,最一開始是所有介面都要遵循的大重點,也是今天等等會介紹的守則;接下來則是針對各種方面的守則,包含 正確性、效率、封裝、維護、擴展性與慣例遵循等等。當然要注意的事情無限多,這些守則就是針對最重要的觀念做介紹,列出一些常見錯誤,以及在 class, function跟template設計中常見的問題與解法。
馬上來看看重中之重─第四章的第一條守則吧!
今天的守則是:
Make interfaces easy to use correctly and hard to use incorrectly
這就是設計interface的 大原則。無論是function interface, class interface還是template interface,我們都應該以這個為目標。所謂interface就是我們的code與client/ user互動的入口,而使用者應該都想要 正確地使用你的interface,如果它用錯了,那這個interface的設計者就難辭其咎了,所以我們的設計應該要讓使用者難以用錯,最最理想的目標就是─如果用錯了就不能編譯,如果能編譯那行為就應該符合預期;再用比較少的字來描述─ 能動=正確;不正確=不能動。這樣user想錯用也用不了,直接被compiler把關,能動的就是對的結果。
而上面寫的是理想的方向,而現在我們來具體的看看要怎麼達成這種設計。要做出這種設計,首先需要思考的就是 user可能會怎麼錯用。例如,我們設計了一個class來代表時間:
class Date
{
public:
Date(int month, int day, int year);
}
可以想像到user用的時候可能會犯兩種錯:
1. 傳錯順序:例如把月份與年份弄反。
Date d(30, 3, 1995);
2. 不合理的數值:例如弄出2月30日。
Date d(2, 30, 1995);
下一步就是想辦法讓這些情況,可以被偵測出來並不給使用。
很多可能會出現的error都可以藉由我們自訂的new type來避免。
例如在上面這個例子,我們可以用簡單的 wrapper type 來規範年月日,並在Date
的constructor裡面使用它們。
struct Day
{
explicit Day(int d): val(d){}
int val;
};
struct Month
{
explicit Month(int m): val(m){}
int val;
};
struct Year
{
explicit Day(int y): val(y){}
int val;
};
class Date
{
public:
Date(const Month& m, const Day& d, const Year& y);
};
Date d(30, 3, 1995); // error! wrong types, require Month, Day, Year
Date d(Day(30), Month(3), Year(1995)); // error! wrong types, require Month, Day, Year
Date d(Month(3), Day(30), Year(1995)); // correct
這樣就能初步過濾掉第一種傳錯順序的情形,也避免掉其他可能錯用到別的type的狀況。而要避免掉第二種不合理的值,我們也有幾種方式可以對他們加以設計,例如用 enum 來列出所有可能的值;然而,enum有其他type-safe問題─C++11之後建議改成 enum class 來使用避免這個問題─這個我們先略過,enum/ enum class的使用可參考其他參考資料,這邊我們先不用enum,用相對安全的方式,來將所有合理的Month
值列出:
class Month
{
public:
static Month Jan() { return Month(1); }
static Month Feb() { return Month(2); }
...
private:
explicit Mont(int m);
};
Date d(Month::Mar(), Day(30), Year(1995));
這就是使用Month()
function取代原本的object的方式。
還有一個避免誤用的方式就是針對new type限制它可做的行為,例如─加上 const
。例如我們在前面的守則有講過的([Day 5] Use const whenever possible (1)),在operator*
return type加上const
可以避免像下面這種誤用的行為:
if(a*b=c)... // error!
從上面這個可以延伸出另外一個守則─ 讓你的types行為跟built-in type一致。因為user已經熟知built-in type是怎麼使用的,所以最好讓你的type也一樣,這樣user就自然而然地知道該怎麼用。像上面的例子,假設有int a
與int b
,指派值給a*b
是不合法的行為,所以除非有特殊理由,最好也讓自己定義的type也遵循一樣的規則。這個 consistency─一致性是其中的精髓。就像STL的容器們有大量的共通規則,就讓它們變得比較容易使用,例如所有STL容器都有size()
這個member function。
明天,我們繼續來看一致性的實例,以及為什麼比較少的特殊規則可以讓interface變得容易使用,結束這個守則。